package de.mmis.core.base.util;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import de.mmis.core.base.ObjectDescriptor.Attributes;
import de.mmis.core.base.Toolbox;
import de.mmis.core.base.Type;

public class ClassToXMLConverter {
	private ClassToXMLConverter() {
		// no constructor
	}

	public static String toXMLDescription(Class<?> clazz,
			ClassLoader classLoader) throws Exception {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		Document doc = buildDocument(clazz, classLoader);

		outputDocument(doc, baos);

		return Toolbox.toUTF8String(baos.toByteArray());
	}

	private static Document buildDocument(Class<?> clazz,
			ClassLoader classLoader) throws ClassToXMLException {
		// We need a Document
		DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder;
		try {
			docBuilder = dbfac.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			throw new ClassToXMLException("parser configuration failed", e);
		}
		Document doc = docBuilder.newDocument();

		Element root;
		try {
			root = buildClassElement(clazz, classLoader, doc);
		} catch (ClassConvertingException e) {
			throw new ClassToXMLException("error converting class \""
					+ clazz.getName() + "\"", e);
		}

		doc.appendChild(root);
		return doc;
	}

	private static Element buildClassElement(Class<?> clazz,
			ClassLoader classLoader, Document doc)
			throws ClassConvertingException {
		Element ret = doc.createElement("class");
		ret.setAttribute("name",
				Type.getAbstractNameFromClass(clazz, classLoader));
		ret.setAttribute("interface", clazz.isInterface() ? "yes" : "no");

		try {
			addAttributes(ret, clazz.getAnnotation(Attributes.class));
		} catch (InvalidAttributeException e) {
			throw new ClassConvertingException(
					"error adding attributes of class \"" + clazz.getName()
							+ "\"", e);
		}

		if (clazz.getSuperclass() != null)
			ret.appendChild(buildClassElement(clazz.getSuperclass(),
					classLoader, doc));
		for (Class<?> c : clazz.getInterfaces())
			ret.appendChild(buildClassElement(c, classLoader, doc));

		for (Method m : clazz.getDeclaredMethods()) {
			Element me = doc.createElement("method");
			me.setAttribute("name", m.getName());
			me.setAttribute("returnType", Type.getAbstractNameFromClass(
					m.getReturnType(), classLoader));

			String visibility = "default";
			if (Modifier.isPublic(m.getModifiers()))
				visibility = "public";
			else if (Modifier.isProtected(m.getModifiers()))
				visibility = "protected";
			else if (Modifier.isPrivate(m.getModifiers()))
				visibility = "private";
			me.setAttribute("visibility", visibility);
			me.setAttribute("static",
					Modifier.isStatic(m.getModifiers()) ? "yes" : "no");

			try {
				addAttributes(me, m.getAnnotation(Attributes.class));
			} catch (InvalidAttributeException e) {
				throw new ClassConvertingException(
						"error adding attributes of method \"" + m.getName()
								+ "\"", e);
			}

			for (int i = 0; i < m.getParameterTypes().length; i++) {
				Class<?> c = m.getParameterTypes()[i];
				Element tmp = doc.createElement("parameter");
				tmp.setAttribute("type",
						Type.getAbstractNameFromClass(c, classLoader));

				Attributes att = null;
				for (Annotation an : m.getParameterAnnotations()[i])
					if (an instanceof Attributes)
						att = (Attributes) an;
				try {
					addAttributes(tmp, att);
				} catch (InvalidAttributeException e) {
					throw new ClassConvertingException(
							"error adding parameter attributes of method \""
									+ m.getName() + "\"", e);
				}

				me.appendChild(tmp);
			}
			ret.appendChild(me);
		}

		return ret;
	}

	private static void addAttributes(Element elem, Attributes att)
			throws InvalidAttributeException {
		if (att != null) {
			for (String a : att.value()) {
				int sepIndex = a.indexOf("=");
				if (sepIndex == -1)
					throw new InvalidAttributeException(
							"no equals sign found in attribute \"" + a + "\"");
				String key = a.substring(0, sepIndex);
				String value = a.substring(sepIndex + 1);
				while (key.endsWith(" "))
					key = key.substring(0, key.length() - 1);
				while (value.startsWith(" "))
					value = value.substring(1);
				Element tmp = elem.getOwnerDocument()
						.createElement("attribute");
				tmp.setAttribute("key", key);
				tmp.setAttribute("value", value);
				elem.appendChild(tmp);
			}
		}
	}

	public static class InvalidAttributeException extends Exception {
		public InvalidAttributeException() {
			super();
		}

		public InvalidAttributeException(String message) {
			super(message);
		}
	}

	public static class ClassConvertingException extends Exception {
		public ClassConvertingException() {
			super();
		}

		public ClassConvertingException(String message) {
			super(message);
		}

		public ClassConvertingException(String message, Throwable cause) {
			super(message, cause);
		}
	}

	public static class ClassToXMLException extends Exception {
		public ClassToXMLException() {
			super();
		}

		public ClassToXMLException(String message) {
			super(message);
		}

		public ClassToXMLException(String message, Throwable cause) {
			super(message, cause);
		}
	}

	private static void outputDocument(Document doc, OutputStream out)
			throws UnsupportedEncodingException, TransformerException {
		TransformerFactory transfac = TransformerFactory.newInstance();
		transfac.setAttribute("indent-number", new Integer(2));
		Transformer trans = transfac.newTransformer();
		trans.setOutputProperty(OutputKeys.INDENT, "yes");

		// create string from xml tree
		OutputStreamWriter sw = new OutputStreamWriter(out, "UTF-8");
		StreamResult result = new StreamResult(sw);
		DOMSource source = new DOMSource(doc);
		trans.transform(source, result);
	}
}
